function doBackup()
{
	var background = Cc["@mozilla.org/thread-manager;1"]
		.getService()
		.newThread(0);
	
	background.dispatch(new backupThread(), Ci.nsIThread.DISPATCH_NORMAL);
}

/* Create Backup Thread Singleton */
var backupThread = function()
{
	this.start = new Date().getTime();
	this.games = new Array();
	this.count = 0;
	this.total = 0;

	notifyObservers(null, 'backup-begin', '');

	/* Get GameList ListBox Object */
	var gamelist = document.getElementById('gamelist');
	if(!gamelist)
	{
		notifyObservers(null, 'backup-failed', 'Unable to find gamelist listbox.');
		return;
	}

	/* Iterate through Each Game in Game List */
	for(var i = 0; i < gamelist.getRowCount(); i++)
	{
		try
		{
			var row = gamelist.getItemAtIndex(i);
		}
		catch(ex)
		{
			log('Unable to get row from game list.', SD_LOG_WARN);
			continue;
		}
	
		/* Evaluate Game JSON from ListItem Row */
		try
		{
			var game = eval(row.value);
		}
		catch(ex)
		{
			log('Unable to parse game definition data.', SD_LOG_WARN);
			continue;
		}

		this.games.push(game);
	}
};

backupThread.prototype = {
	run: function()
	{
		notifyObservers(null, 'status-changed', 'Backing up collection...');

		var backupDir = gPrefs['backup.destination'];
		var compression = gPrefs['backup.archive.compression'];
		var tempRemove = gPrefs['temp.remove'];

		log('Backing up collection.', SD_LOG_INFO);

		/* Get Temp Directory */
		try
		{
			var tmpDir = Cc["@mozilla.org/file/directory_service;1"]
				.getService(Ci.nsIProperties)
				.get('TmpD', Ci.nsIFile);
			tmpDir.append('sDB');
		}
		catch(ex)
		{
			notifyObservers(null, 'backup-failed', 'Unable to get temp directory.');
			return;
		}

		log('Temp Directory: ' + tmpDir.path, SD_LOG_DEBUG2);
	
		/* Create Collection Temp Directory */
		try
		{
			var collectionDir = tmpDir.clone();
			collectionDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0777);
		}
		catch(ex)
		{
			notifyObservers(null, 'backup-failed', 'Unable to create collection temp directory.');
			return;
		}	

		log('Collection Directory: ' + collectionDir.path, SD_LOG_DEBUG2);
	
		/* Determine Collection Name */
		var collectionDateObj = new Date();
		var collectionName = ''
			+ collectionDateObj.getFullYear()
			+ collectionDateObj.getMonth()
			+ collectionDateObj.getDay()
			+ collectionDateObj.getHours()
			+ collectionDateObj.getMinutes();
		
		/* Create Collection Archive File */
		var collectionZip = backupDir.clone();
		collectionZip.append(collectionName + '.sdca');

		log('Collection Zip: ' + collectionZip.path, SD_LOG_DEBUG2);
	
		/* Get Zip Writer */
		try
		{
			zwCollection = Cc["@mozilla.org/zipwriter;1"]
				.createInstance(Ci.nsIZipWriter);
			zwCollection.open(collectionZip, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE | PR_SYNC);
		}
		catch(ex)
		{
			notifyObservers(null, 'backup-failed', 'Unable to create collection archive file.');
			return;
		}
	
		/* Create Collection Games Directory */
		try
		{
			var collectionGameDir = collectionDir.clone();
			collectionGameDir.append('games');
			collectionGameDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0777);
		}
		catch(ex)
		{
			notifyObservers(null, 'backup-failed', 'Unable to create games directory in temp directory.');
			return;
		}

		log('Collection Game Directory: ' + collectionGameDir.path, SD_LOG_DEBUG2);
		
		/* Add Games Directory to Collection Archive */
		try
		{
			zwCollection.addEntryDirectory('games', Date.now() * PR_USEC_PER_MSEC, false);
		}
		catch(ex)
		{
			notifyObservers(null, 'backup-failed', 'Unable to add games directory to collection archive.');
			return;
		}

		/* Create Collection Journal */
		var collectionJournal = new Object();
       		collectionJournal.games = new Array();
		collectionJournal.timestamp = new Date().getTime();
	
		/* Determine Total Number of Records */
		for(var i = 0; i < this.games.length; i++)
		{
			/* Get Game Object from Games List */
			var game = this.games[i];

			var recordList = new Array();
			for(var j = 0; j < game.fsData.length; j++)
			{
				try
				{
					var entry = Cc["@mozilla.org/file/local;1"]
						.createInstance(Ci.nsILocalFile);
					entry.initWithPath(game.fsData[j]);
				}
				catch(ex)
				{
					continue;
				}
				
				if(entry.exists())
				{
					// Recursively Find Files
					recordList = recordList.concat(this._getFiles(entry));
				}
				else
				{
					continue;
				}
			}
	
			this.total += recordList.length;
	
			var recordList = new Array();
			for(var j = 0; j < game.regData.length; j++)
			{
				/* Split Entry into Hive/Key */
				try
				{
					var keyParts = game.regData[j].split('\\');
					var hive = keyParts.shift();
					var key = keyParts.join('\\');
				}
				catch(ex)
				{
					continue;
				}
				
				var wrk = Cc["@mozilla.org/windows-registry-key;1"]
					.createInstance(Ci.nsIWindowsRegKey);
	
				/* Define Hive Aliases */
				var hives = {
					HKEY_LOCAL_MACHINE: Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
					HKEY_CURRENT_USER: Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
					HKEY_CLASSES_ROOT: Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT
				};
			
				/* Open Registry Key */
				try
				{
					wrk.open(hives[hive], key, Ci.nsIWindowsRegKey.ACCESS_READ);
				}
				catch(ex)
				{
					continue;
				}
				
				/* Create Registry Key Object */
				var wrkObj = {
					entry: wrk,
					hive: hive,
					key: key
				};
	
				// Recursively Find Keys
				try
				{
					recordList = recordList.concat(this._getRegKeys(wrkObj));
				}
				catch(ex)
				{
					continue;
				}

				wrk.close();
			}

			this.total += recordList.length;
		}

		log('Total Entries: ' + this.total, SD_LOG_DEBUG2);

		/* Iterate through Each Game in Game List */
		for(var i = 0; i < this.games.length; i++)
		{
			var game = this.games[i];

			notifyObservers(null, 'status-changed', 'Backing up ' + game.name);

			log('Backing up ' + game.name, SD_LOG_DEBUG);

			/* Add Game to Collection Journal */
			collectionJournal.games.push(game);
	
			/* Create Game Journal */
			var gameJournal = new Object();
			gameJournal.fsData = new Array();
			gameJournal.regData = new Array();
			gameJournal.timestamp = new Date().getTime();
	
			/* Create Game Temp Directory */
			try
			{
				var gameDir = tmpDir.clone();	
				gameDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0777);
			}
			catch(ex)
			{
				log('Unable to create game temp directory.', SD_LOG_WARN);
				continue;
			}

			log('Game Directory: ' + gameDir.path, SD_LOG_DEBUG2);
	
			/* Create Game Zip File */
			var gameZip = collectionGameDir.clone();
			gameZip.append(game.uuid + '.sdga');
			
			/* Get Zip Writer */
			try
			{
				var zwGame = Cc["@mozilla.org/zipwriter;1"]
					.createInstance(Ci.nsIZipWriter);
				zwGame.open(gameZip, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE | PR_SYNC);
			}
			catch(ex)
			{
				log('Unable to create game archive file.', SD_LOG_WARN);
				continue;
			}

			log('Game Archive: ' + gameZip.path, SD_LOG_DEBUG2);
	
			/* Create Filesystem Data Directory */
			try
			{
				var fsDataDir = gameDir.clone();
				fsDataDir.append('fsdata');
				fsDataDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0777);
			}
			catch(ex)
			{
				log('Unable to create filesystem data directory in temp directory.', SD_LOG_WARN);
				continue;
			}

			log('fsData Directory: ' + fsDataDir.path, SD_LOG_DEBUG2);
	
			/* Add Filesystem Data Directory to Game Archive */
			try
			{
				zwGame.addEntryDirectory('fsdata', Date.now() * PR_USEC_PER_MSEC, false);
			}
			catch(ex)
			{
				log('Unable to add filesystem data directory to game archive.', SD_LOG_WARN);
				continue;
			}
	
			/* Create RegData Directory */
			try
			{
				var regDataDir = gameDir.clone();
				regDataDir.append('regdata');
				regDataDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0777);
			}
			catch(ex)
			{
				log('Unable to create registry data directory in temp directory.', SD_LOG_WARN);
				continue;
			}

			log('regData Directory: ' + regDataDir.path, SD_LOG_DEBUG2);
	
			/* Add Registry Data Directory to Game Archive */
			try
			{
				zwGame.addEntryDirectory('regdata', Date.now() * PR_USEC_PER_MSEC, false);
			}
			catch(ex)
			{
				log('Unable to add registry data directory to game archive.', SD_LOG_WARN);
				continue;
			}
	
			/* Copy Game Definition File */
			try
			{
				var gameFile = Cc["@mozilla.org/file/local;1"]
					.createInstance(Ci.nsILocalFile);
				gameFile.initWithPath(game.file);
				gameFile.copyTo(gameDir, 'game.sdgd');
			}
			catch(ex)
			{
				log('Unable to copy game definition to temp directory.', SD_LOG_WARN);
				continue;
			}
	
			/* Add Game Definition File to Game Archive */
			try
			{
				zwGame.addEntryFile("game.sdgd", compression, gameFile, false);
			}
			catch(ex)
			{
				log('Unable to add game definition file to game archive.', SD_LOG_WARN);
				continue;
			}
	
			/* Create List of Files to Backup */
			var backupList = new Array();
			for(var j = 0; j < game.fsData.length; j++)
			{
				try
				{
					var entry = Cc["@mozilla.org/file/local;1"]
						.createInstance(Ci.nsILocalFile);
					entry.initWithPath(game.fsData[j]);
				}
				catch(ex)
				{
					log('Unable to parse game file path.', SD_LOG_WARN);
					continue;
				}
				
				if(entry.exists())
				{
					// Recursively Find Files
					backupList = backupList.concat(this._getFiles(entry));
				}
				else
				{
					log('Unable to locate game file.', SD_LOG_WARN);
					continue;
				}
			}
	
			/* Backup Files */
			for(var j = 0; j < backupList.length; j++)
			{
				/* Compute MD5Sum for Game File */
				try
				{
					/* Open Stream for File */
					var istream = Cc["@mozilla.org/network/file-input-stream;1"]
						.createInstance(Ci.nsIFileInputStream);
					istream.init(backupList[j], PR_RDONLY, 0444, Ci.nsIFileInputStream.CLOSE_ON_EOF);
					
					var ch = Cc["@mozilla.org/security/hash;1"]
						.createInstance(Ci.nsICryptoHash);
					ch.init(Ci.nsICryptoHash.MD5);
	
					var md5 = 'd41d8cd98f00b204e9800998ecf8427e';
					if(backupList[j].fileSize > 0)
					{
						/* Create Hash from Stream Data */
						ch.updateFromStream(istream, PR_UINT32_MAX);
						var hash = ch.finish(false);
	
						/* Convert Binary Hash to MD5 String */
						md5 = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
					}
				}
				catch(ex)
				{
					log('Unable to compute md5sum for file.', SD_LOG_WARN);
					continue;
				}

				/* Copy File to MD5Sum File */
				var fsDataFile = fsDataDir.clone();
				fsDataFile.append(md5);
	
				log('Copying ' + backupList[j].path + ' to ' + fsDataFile.path, SD_LOG_DEBUG);
				if(!fsDataFile.exists())
				{
					/* Copy File */
					try
					{
						backupList[j].copyTo(fsDataDir, md5);
					}
					catch(ex)
					{
						log('Unable to copy game file to temp directory.', SD_LOG_WARN);
						continue;
					}

					/* Add File to Game Archive */
					try
					{
						zwGame.addEntryFile('fsdata/' + md5, compression, fsDataFile, false);
					}
					catch(ex)
					{
						log('Unable to add file to game archive.', SD_LOG_WARN);
						continue;
					}
				}
	
				/* Create Journal Entry */
				var journalObj = new Object();
				journalObj.path = backupList[j].path;
				journalObj.md5 = md5;
				
				gameJournal.fsData.push(journalObj);
	
				this.count += 1;

				notifyObservers(null, 'progress-changed', [this.count, this.total]);
			}
	
			/* Backup Registry Entries */
			for(var j = 0; j < game.regData.length; j++)
			{
				/* Split Entry into Hive/Key */
				try
				{
					var keyParts = game.regData[j].split('\\');
					var hive = keyParts.shift();
					var key = keyParts.join('\\');
				}
				catch(ex)
				{
					log('Unable to parse registry entry.', SD_LOG_WARN);
					continue;
				}
				
				var wrk = Cc["@mozilla.org/windows-registry-key;1"]
					.createInstance(Ci.nsIWindowsRegKey);
	
				/* Define Hive Aliases */
				var hives = {
					HKEY_LOCAL_MACHINE: Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
					HKEY_CURRENT_USER: Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
					HKEY_CLASSES_ROOT: Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT
				};
			
				/* Open Registry Key */
				try
				{
					wrk.open(hives[hive], key, Ci.nsIWindowsRegKey.ACCESS_READ);
				}
				catch(ex)
				{
					log('Unable to open registry key.', SD_LOG_WARN);
					continue;
				}
				
				/* Create Registry Key Object */
				var wrkObj = {
					entry: wrk,
					hive: hive,
					key: key
				};
	
				// Recursively Find Keys
				try
				{
					var backupList = this._getRegKeys(wrkObj);
				}
				catch(ex)
				{
					log('Unable to retrieve registry values.', SD_LOG_WARN);
					continue;
				}

				/* Close Registry Key */ 
				try
				{
					wrk.close();
				}
				catch(ex)
				{
					log('Unable to close registry key.', SD_LOG_WARN);
				}
	
				/* Create MD5 Hash of Path for Storage */
				try
				{
					var uniconv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
						.createInstance(Ci.nsIScriptableUnicodeConverter);
					uniconv.charset = 'UTF-8';
	
					var path = hive + '\\' + key;
					var pathResult = {};
					var pathData = uniconv.convertToByteArray(path, pathResult);
	
					var ch = Cc["@mozilla.org/security/hash;1"]
						.createInstance(Ci.nsICryptoHash);
					ch.init(Ci.nsICryptoHash.MD5);
					ch.update(pathData, pathData.length);
	
					var hash = ch.finish(false);
					var md5 = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
				}
				catch(ex)
				{
					log('Unable to compute MD5 hash for registry key.', SD_LOG_WARN);
					continue;
				}

				/* Write Data to new MD5 Hash File */
				var regDataFile = regDataDir.clone();
				regDataFile.append(md5);
	
				log('Exporting registry data to ' + regDataFile.path, SD_LOG_DEBUG);
				try
				{
					var foStream = Cc["@mozilla.org/network/file-output-stream;1"]
						.createInstance(Ci.nsIFileOutputStream);
	
					foStream.init(regDataFile, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE | PR_SYNC, 0666, 0);
			
					var cos = Cc["@mozilla.org/intl/converter-output-stream;1"]
						.createInstance(Ci.nsIConverterOutputStream);
					cos.init(foStream, "UTF-8", 0, 0);
					cos.writeString(backupList.toSource());
					cos.close();
				}
				catch(ex)
				{
					log('Unable to write registry file to temp directory.', SD_LOG_WARN);
					continue;
				}
	
				/* Add File to Game Archive */
				try
				{
					zwGame.addEntryFile('regdata/' + md5, compression, regDataFile, false);
				}
				catch(ex)
				{
					log('Unable to add registry file to game archive.', Sd_LOG_WARN);
					continue;
				}
	
				/* Create Journal Entry */
				var journalObj = new Object();
				journalObj.path = path;
				journalObj.md5 = md5;
					
				gameJournal.regData.push(journalObj);

				this.count += backupList.length;

				notifyObservers(null, 'progress-changed', [this.count, this.total]);
			}
	
			/* Write Game Journal File */
			var gameJournalFile = gameDir.clone();
			gameJournalFile.append('journal.json');
	
			log('Exporting game journal to ' + gameJournalFile.path, SD_LOG_DEBUG);
			try
			{
				var foStream = Cc["@mozilla.org/network/file-output-stream;1"]
					.createInstance(Ci.nsIFileOutputStream);

				foStream.init(gameJournalFile, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE | PR_SYNC, 0666, 0);
				
				var cos = Cc["@mozilla.org/intl/converter-output-stream;1"]
					.createInstance(Ci.nsIConverterOutputStream);
				cos.init(foStream, "UTF-8", 0, 0);
				cos.writeString(gameJournal.toSource());
				cos.close();
			}
			catch(ex)
			{
				log('Unable to export game journal to temp directory.', SD_LOG_WARN);
				continue;
			}
	
			/* Add Game Journal to Game Archive */
			try
			{
				zwGame.addEntryFile('journal.json', compression, gameJournalFile, false);
			}
			catch(ex)
			{
				log('Unable to add game journal to game archive.', SD_LOG_WARN);
				continue;
			}

			/* Add Comment to Game Archive */
			try
			{
				zwGame.comment = 'SaveDB Game Archive';
			}
			catch(ex)
			{
				log('Unable to add comment to game archive.', SD_LOG_WARN);
			}

			/* Close Game Archive */
			try
			{
				zwGame.close();
			}
			catch(ex)
			{
				log('Unable to close game archive.', SD_LOG_WARN);
				continue;
			}
	
			/* Remove Game Temp Directory */
			try
			{
				if(tempRemove)
				{
					gameDir.remove(true);
				}
			}
			catch(ex)
			{
				log('Unable to remove game temporary directory.', SD_LOG_WARN);
			}

			/* Add Game Archive to Collection Archive */
			try
			{
				zwCollection.addEntryFile('games/' + game.uuid + '.sdga', Ci.nsIZipWriter.COMPRESSION_NONE, gameZip, false);
			}
			catch(ex)
			{
				log('Unable to add game archive to collection archive.', SD_LOG_WARN);
				continue;
			}

			log('Game backup completed successfully.', SD_LOG_DEBUG);
		}

		notifyObservers(null, 'status-changed', 'Cleaning up...');

		/* Write Collection Journal File */
		var collectionJournalFile = collectionDir.clone();
		collectionJournalFile.append('journal.json');

		log('Exporting collection journal to ' + collectionJournalFile.path, SD_LOG_DEBUG);
		try
		{
			var foStream = Cc["@mozilla.org/network/file-output-stream;1"]
				.createInstance(Ci.nsIFileOutputStream);
	
			foStream.init(collectionJournalFile, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE | PR_SYNC, 0666, 0);
			
			var cos = Cc["@mozilla.org/intl/converter-output-stream;1"]
				.createInstance(Ci.nsIConverterOutputStream);
			cos.init(foStream, "UTF-8", 0, 0);
			cos.writeString(collectionJournal.toSource());
			cos.close();
		}
		catch(ex)
		{
			notifyObservers(null, 'backup-failed', 'Unable to write journal file to temp directory.');
			return;
		}
	
		/* Add Collection Journal to Collection Archive */
		try
		{
			zwCollection.addEntryFile('journal.json', compression, collectionJournalFile, false);
		}
		catch(ex)
		{
			notifyObservers(null, 'backup-failed', 'Unable to add collection journal to collection archive.');
			return;
		}

		/* Add Comment to Collection Archive */
		try
		{
			zwCollection.comment = 'SaveDB Game Collection';
		}
		catch(ex)
		{
			log('Unable to add comment to collection archive.', SD_LOG_WARN);
		}
				
		/* Close Collection Archive */
		try
		{
			zwCollection.close();
		}
		catch(ex)
		{
			notifyObservers(null, 'backup-failed', 'Unable to close collection archive.');
			return;
		}
				
		/* Remove Collection Temp Directory */
		try
		{
			if(tempRemove)
			{
				collectionDir.remove(true);
			}
		}
		catch(ex)
		{
			log('Unable to remove collection temporary directory.', SD_LOG_WARN);
		}

		var duration = '';
		var milliseconds = new Date().getTime() - this.start;
		var seconds = Math.ceil((milliseconds / PR_USEC_PER_MSEC));
		var minutes = 0;
		var hours = 0;
	
		if(seconds > 60)
		{
			minutes = (seconds - (seconds % 60)) / 60;
			seconds = seconds % 60;
		}
	
		if(minutes > 60)
		{
			hours = (minutes - (minutes % 60)) / 60;
			minutes = minutes % 60;
		}
	
		if(hours != 0)
		{
			duration += hours + 'h';
		}

		if(minutes != 0)
		{
			duration += minutes + 'm';
		}
	
		if(seconds != 0)
		{
			duration += seconds + 's';
		}

		notifyObservers(null, 'backup-success', '');
	},
	QueryInterface: function(iid)
	{
		if(iid.equals(Ci.nsIRunnable) || iid.equals(Ci.nsISupports))
		{
			return this;
		}
		throw Cr.NS_ERROR_NO_INTERFACE;
	},
	_getFiles: function(entry)
	{
		log('getFiles: ' + entry.path, SD_LOG_DEBUG3);
		var fileList = new Array();
	
		/* Iterate through Directory Entries */
		if(entry.isDirectory())
		{
			var files = entry.directoryEntries;
			while(files.hasMoreElements())
			{
				var file = files.getNext();
	 			file.QueryInterface(Ci.nsIFile);
				fileList = fileList.concat(this._getFiles(file));
			}
		}
	
		/* Add File nsiFile Object to Array */
		if(entry.isFile())
		{
			fileList.push(entry);
		}
	
		return fileList;
	},
	_getRegKeys: function(wrkObj)
	{
		log('getRegKeys: ' + wrkObj.hive + '\\' + wrkObj.key, SD_LOG_DEBUG3);
		var regList = new Array();
		
		/* Iterate through Registry Key Children */
		if(wrkObj.entry.childCount > 0)
		{
			for(var i = 0; i < wrkObj.entry.childCount; i++)
			{
				var childName = wrkObj.entry.getChildName(i);
				var childEntry = wrkObj.entry.openChild(childName, Ci.nsIWindowsRegKey.ACCESS_READ);
	
				/* Create Registry Object */
				var childWrkObj = {
					entry: childEntry,
					hive: wrkObj.hive,
					key: wrkObj.key + '\\' + childName
				}
	
				/* Process Children Keys */
				regList = regList.concat(this._getRegKeys(childWrkObj));
				childEntry.close();
			}
		}
		
		/* Add Registry Values to Array */
		if(wrkObj.entry.valueCount > 0)
		{
			for(var i = 0; i < wrkObj.entry.valueCount; i++)
			{
				var valueName = wrkObj.entry.getValueName(i);
				var valueType = wrkObj.entry.getValueType(valueName);
				var valueValue = readRegistryValue(wrkObj.entry, valueName);
	
				var valueObj =  {
					hive: wrkObj.hive,
					key: wrkObj.key,
					name: valueName,
					type: valueType,
					value: valueValue
				}
					
				regList.push(valueObj);
			}
		}
	
		return regList;
	},
};
